package com.uxsino.simo.query;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Map;
import java.util.TreeMap;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.uxsino.commons.utils.config.PositionalXMLReader;
import com.uxsino.simo.connections.IConnection;
import com.uxsino.simo.connections.NoneConnection;
import com.uxsino.simo.connections.NoneConnection.NoneTarget;
import com.uxsino.simo.connections.target.AbstractTarget;
import com.uxsino.simo.connections.target.ITarget;

public class ProtocolManager {
    private static Logger logger = LoggerFactory.getLogger(ProtocolManager.class);

    private static ObjectMapper mapper = new ObjectMapper();

    public static class ProtocolInfo {
        public String defaultParserName;

        public Class<? extends IConnection<? extends AbstractTarget>> connectionClass;

        public Class<? extends AbstractTarget> targetClass;
    };

    private static Map<String, ProtocolInfo> connectionTypes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

    public static void registerConnectionType(String protocolName,
        Class<? extends IConnection<? extends AbstractTarget>> clsConnector,
        Class<? extends AbstractTarget> targetClass, String defaultRetractorName) {
        ProtocolInfo p = new ProtocolInfo();
        p.defaultParserName = defaultRetractorName;
        p.connectionClass = clsConnector;
        p.targetClass = targetClass;
        connectionTypes.put(protocolName, p);
    }

    public static String getDefaultParserName(String protocolName) {
        ProtocolInfo p = connectionTypes.get(protocolName);

        if (p == null) {
            logger.error("connection type " + protocolName + " not found.");
            return "";
        }
        return p.defaultParserName;
    }

    public static Map<String, ProtocolInfo> getConnectionTypes() {
        return connectionTypes;
    }

    public static Class<? extends ITarget> getTargetType(String protocolName) {
        ProtocolInfo p = connectionTypes.get(protocolName);

        if (p == null) {
            logger.error("connection type " + protocolName + " not found.");
            return null;
        }
        return p.targetClass;
    }

    public static IConnection<? extends AbstractTarget> createConnection(String protocolName) {

        ProtocolInfo p = connectionTypes.get(protocolName);

        if (p == null) {
            logger.error("connection type " + protocolName + " not found.");
            return null;
        }

        IConnection<? extends AbstractTarget> conn;

        try {
            conn = p.connectionClass.newInstance();
            return conn;
        } catch (InstantiationException | IllegalAccessException e) {
            logger.error("failed to create connection type: " + protocolName + ". " + "", e);
        }
        return null;
    }

    public static void loadConnectorsFromXml(URL url, String sourceName) throws IOException {
        loadConnectorsFromXml(url.openStream(), sourceName);
    }

    public static void loadConnectorsFromXml(InputStream is, String sourceName) {
        Document doc = PositionalXMLReader.getNormalizedDocumentFromStream(is, sourceName);
        loadConnectorsFromXml(doc);
    }

    @SuppressWarnings("unchecked")
    public static void loadConnectorsFromXml(Document doc) {

        doc.getDocumentElement().normalize();
        XPath xp = XPathFactory.newInstance().newXPath();

        NodeList nl;
        try {
            nl = (NodeList) xp.compile("/protocols/*").evaluate(doc, XPathConstants.NODESET);
        } catch (XPathExpressionException e1) {
            logger.error("error reading protocol drivers. " + e1.getMessage());
            return;
        }
        for (int i = 0; i < nl.getLength(); i++) {
            Node nProtocol = nl.item(i);

            if (nProtocol.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            Element eProtocol = (Element) nProtocol;

            String protocolName = eProtocol.getTagName();
            String targetName = eProtocol.getAttribute("target-class");
            String connectorName = eProtocol.getAttribute("connector-class");

            if (targetName == null || connectorName == null || targetName.length() == 0
                    || connectorName.length() == 0) {
                logger.error("missing target-class, template-class or connector-class for protocol: " + protocolName);
                continue;
            }

            Class<? extends IConnection<? extends AbstractTarget>> clsConnector;
            Class<? extends AbstractTarget> clsTarget;

            try {
                clsTarget = (Class<? extends AbstractTarget>) Class.forName(targetName);
                clsConnector = (Class<? extends IConnection<? extends AbstractTarget>>) Class.forName(connectorName);
                registerConnectionType(protocolName, clsConnector, clsTarget, eProtocol.getAttribute("default-parser"));
            } catch (ClassNotFoundException e) {
                logger.error("cannot found class for connector. protocol: {}. connector class {}. target class: {}",
                    protocolName, connectorName, targetName);
            }

        }

        registerConnectionType("none", NoneConnection.class, NoneConnection.NoneTarget.class, null);
    }

    @SuppressWarnings("unchecked")
    public static <T> T createTargetFromJson(String json, String protocolName) {
        Class<? extends ITarget> targetType = getTargetType(protocolName);
        if (targetType == null) {
            return null;
        }
        if (targetType.equals(NoneTarget.class)) {
            return (T) (new NoneConnection.NoneTarget());
        }
        T target = null;
        try {
            target = (T) mapper.readValue(json, targetType);
        } catch (Exception e) {
            logger.error("Error loading target from json:" + json + "", e);
            return null;
        }

        return target;
    }

}
